package com.ljw.security.springsecurity_springboot.config;

import com.ljw.security.springsecurity_springboot.dao.UserDao;
import com.ljw.security.springsecurity_springboot.model.PermissionDto;
import com.ljw.security.springsecurity_springboot.model.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.stream.Collectors;

/**
 * @FileName WebSecurityConfig
 * @Description TODO     springsecurity配置：1. 自定义接口UserDetailsService的子类  2. 密码编码器  3. 2种授权方式（web授权：url授权;方法注解授权）
 *          2种授权方式（web授权：url授权;方法注解授权），选一即可：
 *              1. web授权：url授权(访问资源url的用户需要拥有该资源url所要求的权限)，如下面的方法configure(HttpSecurity http)中所示为url赋权
 *              2. 方法注解授权。
 *                      步骤（1），在任何 @Configuration 实例上使用 @EnableGlobalMethodSecurity(prePostEnabled = true） 注释来启用基于注解的安全性
 *                      步骤（2），在controller类方法上，标注注解。例子：p1、p2是数据库中 url资源（如/r/r1） 对应的 权限
 *                              @PreAuthorize(value = "isAuthenticated() and hasAnyAuthority('p1','p2')")
 *                                      //必须得认证，然后用户有p1、p2中的任意一个权限即可访问该方法
 *
 *                              @PreAuthorize(value = "isAnonymous() or isAuthenticated()")//匿名或认证用户都可访问
 *
 * @Author ljw
 * @Date 2020/8/31 17:58
 * @Version 1.0
 */
//可以在任何 @Configuration 实例上使用 @EnableGlobalMethodSecurity 注释来启用基于注解的安全性
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    //配置用户信息服务
    //@Bean
    /*protected UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("zhangsan").password("123").authorities(new String[]{"p1"}).build());
        manager.createUser(User.withUsername("lisi").password("456").authorities(new String[]{"p2"}).build());
        return manager;
    }*/

    /**
     * 1. 自定义接口UserDetailsService的子类
     * (1). 自定义类实现接口UserDetailsService，实现接口方法loadUserByUsername，通过用户名获取用户信息
     * (2). 通过@Component注解将自定义类加入spring容器
     */
    @Component
    class SpringDataUserDetailsService implements UserDetailsService {
        
        @Autowired
        UserDao userDao;
        //通过用户名从数据库获取用户信息
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            //登录账号
            System.out.println("username=" + username);
            //根据账号去数据库查询...
            // 这里暂时使用静态数据
            //UserDetails userDetails = User.withUsername(username).password("123").authorities("p1").build();
            /*UserDetails userDetails = User.withUsername(username).password("$2a$10$QhxaUOe.skTo0weH.Q7v5uOOm3GtfLkcrnTdBVMQG7tAGoju1vPGO")
                    .authorities("p1").build();*/


            //根据账号从数据库查询用户信息
            UserDto user = userDao.getUserByUsername(username);
            if (user==null){
                return null;
            }
            //根据userId从数据库查询用户权限信息
            List<PermissionDto> permissions = userDao.findPermissionsByUserId(user.getId());
            String[] strings = new String[0];
            if (permissions!=null){
                //用户权限数组
                strings = permissions.stream().map(PermissionDto::getCode).toArray(String[]::new);
            }

            return User.withUsername(user.getUsername()).password(user.getPassword()).authorities(strings).build();
        }
    }

    /**
     * 2. 密码编码器  返回PasswordEncoder类型
     * (1) NoOpPasswordEncoder采用字符串匹配方法，不对密码进行加密比较处理
     * (2)  实际项目中推荐使用BCryptPasswordEncoder, Pbkdf2PasswordEncoder, SCryptPasswordEncoder等,使用方式如下
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        // return NoOpPasswordEncoder.getInstance();
        return new BCryptPasswordEncoder();
    }

    //3. 配置安全拦截机制--授权（web授权：url授权）
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        /*spring security为防止CSRF（Cross-site request forgery跨站请求伪造）的发生，限制了除了get以外的大多数方 法。
        *       解决方法1： 屏蔽CSRF控制，即spring security不再限制CSRF。
        *
        * */
        http.csrf().disable();  //屏蔽CSRF控制，即spring security不再限制CSRF

        /*会话控制：我们可以通过以下选项准确控制会话何时创建以及Spring Security如何与之交互
            （1）always 如果没有session存在就创建一个
            （2）ifRequired 默认情况下，Spring Security会为每个登录成功的用户会新建一个Session，就是ifRequired 。
            (3)若使用stateless，则说明Spring Security对登录成功的用户不会创建Session了，你的应用程序也不会允许新建 session。
                并且它会暗示不使用cookie，所以每个请求都需要重新进行身份验证。这种无状态架构适用于REST API 及其无状态认证机制,
                即基于Token的认证授权方式时用stateless。
        * */
        http.sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                .invalidSessionUrl("/login-view?error=INVALID_SESSION");//session超时无效之后跳转的路径。invalidSession指传入的sessionid无效




        //web授权：给url授权；还有方法授权：给controller方法授权，使用注解@PreAuthorize(value = "isAnonymous() or isAuthenticated()")
        http.authorizeRequests()
                //下面4行是web授权操作，可用方法授权替换
                //.antMatchers("/r/r1").access("hasAuthority('p1') and hasAuthority('p2')")    // 访问/r/r1资源的 用户需要同时拥有p1和p2权限
               // .antMatchers("/r/r2").hasAnyAuthority("p1","p2")    // 访问/r/r2资源的 用户需要拥有p1或p2权限
                //.antMatchers("/r/**").authenticated()   // url匹配/r/**的资源，经过认证后才能访问
                //.anyRequest().permitAll()                               //其他url资源不经认证，都可以匿名访问
                .and()
                //下面是form表单操作
                .formLogin()                                  //支持form表单认证，
                .loginPage("/login-view")     // 指定我们自己的登录页,spring security以重定向方式跳转到/login-view,/login-view映射到webapp/WEB-INF/view/login.jsp
                .loginProcessingUrl("/login") //  指定登录处理URL，即表单提交路径：action="login"，“login”也是spring security默认的接收用户名、密码的路径
                .successForwardUrl("/login-success")            //认证成功后转向controller方法的路径：/login-success。
                .permitAll()  //我们必须允许所有用户访问我们的登录页（例如为验证的用户），
                                // 这个 formLogin().permitAll() 方法允许 任意用户访问基于表单登录的所有的URL。
                .and()//下面是退出登录操作
                        /*当退出操作出发时，将发生：
                            1.使HTTP Session 无效。session无效后，又跳转到/login-view?error=INVALID_SESSION
                                这是由会话控制的设置导致的 ：.invalidSessionUrl("/login-view?error=INVALID_SESSION");
                            2. 清除 SecurityContextHolder
                            3. 跳转到 /login-view?logout*/
                .logout()
                .logoutUrl("/logout")  //）设置触发退出操作的URL (默认是 /logout ).
                .logoutSuccessUrl("/login-view?logout"); //退出之后跳转的URL。默认是 /login?logout 。


    }


}
